/*
 * Copyright 2005-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ws.client.core;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;

import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.ws.FaultAwareWebServiceMessage;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.WebServiceIOException;
import org.springframework.ws.client.WebServiceTransformerException;
import org.springframework.ws.client.WebServiceTransportException;
import org.springframework.ws.client.support.WebServiceAccessor;
import org.springframework.ws.client.support.destination.DestinationProvider;
import org.springframework.ws.client.support.interceptor.ClientInterceptor;
import org.springframework.ws.context.DefaultMessageContext;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.soap.client.core.SoapFaultMessageResolver;
import org.springframework.ws.support.DefaultStrategiesHelper;
import org.springframework.ws.support.MarshallingUtils;
import org.springframework.ws.transport.FaultAwareWebServiceConnection;
import org.springframework.ws.transport.TransportException;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.WebServiceMessageSender;
import org.springframework.ws.transport.context.DefaultTransportContext;
import org.springframework.ws.transport.context.TransportContext;
import org.springframework.ws.transport.context.TransportContextHolder;
import org.springframework.ws.transport.http.HttpUrlConnectionMessageSender;
import org.springframework.ws.transport.support.TransportUtils;

/**
 * <strong>The central class for client-side Web services.</strong> It provides a
 * message-driven approach to sending and receiving {@link WebServiceMessage} instances.
 * <p>
 * Code using this class need only implement callback interfaces, provide {@link Source}
 * objects to read data from, or use the pluggable {@link Marshaller} support. For
 * invoking the {@link #marshalSendAndReceive marshalling methods}, the
 * {@link #setMarshaller(Marshaller) marshaller} and {@link #setUnmarshaller(Unmarshaller)
 * unmarshaller} properties must be set.
 * <p>
 * This template uses a {@link SoapFaultMessageResolver} to handle fault response
 * messages. Another {@link FaultMessageResolver} can be defined with with
 * {@link #setFaultMessageResolver(FaultMessageResolver) faultMessageResolver} property.
 * If this property is set to {@code null}, no fault resolving is performed.
 * <p>
 * This template uses the following algorithm for sending and receiving.
 * <ol>
 * <li>Call {@link #createConnection(URI) createConnection()}.</li>
 * <li>Call {@link WebServiceMessageFactory#createWebServiceMessage()
 * createWebServiceMessage()} on the registered message factory to create a request
 * message.</li>
 * <li>Invoke {@link WebServiceMessageCallback#doWithMessage(WebServiceMessage)
 * doWithMessage()} on the request callback, if any. This step stores content in the
 * request message, based on {@code Source}, marshalling, etc.</li>
 * <li>Invoke {@link ClientInterceptor#handleRequest(MessageContext) handleRequest()} on
 * the registered {@link #setInterceptors(ClientInterceptor[]) interceptors}. Interceptors
 * are executed in order. If any of the interceptors creates a response message in the
 * message context, skip to step 7.</li>
 * <li>Call {@link WebServiceConnection#send(WebServiceMessage) send()} on the
 * connection.</li>
 * <li>Call {@link #hasError(WebServiceConnection,WebServiceMessage) hasError()} to check
 * if the connection has an error. For an HTTP transport, a status code other than
 * {@code 2xx} indicates an error. However, since a status code of 500 can also indicate a
 * SOAP fault, the template verifies whether the error is not a fault.
 * <ul>
 * <li>If the connection has an error, call the {@link #handleError handleError()} method,
 * which by default throws a {@link WebServiceTransportException}.</li>
 * <li>If the connection has no error, continue with the next step.</li>
 * </ul>
 * <li>Invoke {@link WebServiceConnection#receive(WebServiceMessageFactory) receive} on
 * the connection to read the response message, if any.
 * <ul>
 * <li>If no response was received, return {@code null} or {@code false}</li>
 * <li>Call {@link #hasFault(WebServiceConnection,WebServiceMessage) hasFault()} to
 * determine whether the response has a fault. If it has, call
 * {@link ClientInterceptor#handleFault(MessageContext)} and the {@link #handleFault
 * handleFault()} method.</li>
 * <li>Otherwise, invoke {@link ClientInterceptor#handleResponse(MessageContext)} and
 * {@link WebServiceMessageExtractor#extractData(WebServiceMessage) extractData()} on the
 * response extractor, or
 * {@link WebServiceMessageCallback#doWithMessage(WebServiceMessage) doWithMessage} on the
 * response callback.</li>
 * </ul>
 * <li>Call to {@link WebServiceConnection#close() close} on the connection.</li>
 * </ol>
 *
 * @author Arjen Poutsma
 * @since 1.0.0
 */
public class WebServiceTemplate extends WebServiceAccessor implements WebServiceOperations {

	/** Log category to use for message tracing. */
	public static final String MESSAGE_TRACING_LOG_CATEGORY = "org.springframework.ws.client.MessageTracing";

	/** Additional logger to use for sent message tracing. */
	protected static final Log sentMessageTracingLogger = LogFactory
		.getLog(WebServiceTemplate.MESSAGE_TRACING_LOG_CATEGORY + ".sent");

	/** Additional logger to use for received message tracing. */
	protected static final Log receivedMessageTracingLogger = LogFactory
		.getLog(WebServiceTemplate.MESSAGE_TRACING_LOG_CATEGORY + ".received");

	private @Nullable Marshaller marshaller;

	private @Nullable Unmarshaller unmarshaller;

	private @Nullable FaultMessageResolver faultMessageResolver;

	private boolean checkConnectionForError = true;

	private boolean checkConnectionForFault = true;

	private ClientInterceptor @Nullable [] interceptors;

	private @Nullable DestinationProvider destinationProvider;

	/** Creates a new {@code WebServiceTemplate} using default settings. */
	public WebServiceTemplate() {
		initDefaultStrategies();
	}

	/**
	 * Creates a new {@code WebServiceTemplate} based on the given message factory.
	 * @param messageFactory the message factory to use
	 */
	public WebServiceTemplate(WebServiceMessageFactory messageFactory) {
		setMessageFactory(messageFactory);
		initDefaultStrategies();
	}

	/**
	 * Creates a new {@code WebServiceTemplate} with the given marshaller. If the given
	 * {@link Marshaller} also implements the {@link Unmarshaller} interface, it is used
	 * for both marshalling and unmarshalling. Otherwise, an exception is thrown.
	 * <p>
	 * Note that all {@link Marshaller} implementations in Spring also implement the
	 * {@link Unmarshaller} interface, so that you can safely use this constructor.
	 * @param marshaller object used as marshaller and unmarshaller
	 * @throws IllegalArgumentException when {@code marshaller} does not implement the
	 * {@link Unmarshaller} interface
	 * @since 2.0.3
	 */
	public WebServiceTemplate(Marshaller marshaller) {
		Assert.notNull(marshaller, "marshaller must not be null");
		if (!(marshaller instanceof Unmarshaller)) {
			throw new IllegalArgumentException("Marshaller [" + marshaller + "] does not implement the Unmarshaller "
					+ "interface. Please set an Unmarshaller explicitly by using the "
					+ "WebServiceTemplate(Marshaller, Unmarshaller) constructor.");
		}
		else {
			this.setMarshaller(marshaller);
			this.setUnmarshaller((Unmarshaller) marshaller);
		}
		initDefaultStrategies();
	}

	/**
	 * Creates a new {@code MarshallingMethodEndpointAdapter} with the given marshaller
	 * and unmarshaller.
	 * @param marshaller the marshaller to use
	 * @param unmarshaller the unmarshaller to use
	 * @since 2.0.3
	 */
	public WebServiceTemplate(Marshaller marshaller, Unmarshaller unmarshaller) {
		Assert.notNull(marshaller, "marshaller must not be null");
		Assert.notNull(unmarshaller, "unmarshaller must not be null");
		this.setMarshaller(marshaller);
		this.setUnmarshaller(unmarshaller);
		initDefaultStrategies();
	}

	/**
	 * Returns the default URI to be used on operations that do not have a URI parameter.
	 */
	public @Nullable String getDefaultUri() {
		if (this.destinationProvider != null) {
			URI uri = this.destinationProvider.getDestination();
			return (uri != null) ? uri.toString() : null;
		}
		else {
			return null;
		}
	}

	/**
	 * Set the default URI to be used on operations that do not have a URI parameter.
	 * <p>
	 * Typically, either this property is set, or
	 * {@link #setDestinationProvider(DestinationProvider)}, but not both.
	 * @see #marshalSendAndReceive(Object)
	 * @see #marshalSendAndReceive(Object,WebServiceMessageCallback)
	 * @see #sendSourceAndReceiveToResult(Source,Result)
	 * @see #sendSourceAndReceiveToResult(Source,WebServiceMessageCallback,Result)
	 * @see #sendSourceAndReceive(Source,SourceExtractor)
	 * @see #sendSourceAndReceive(Source,WebServiceMessageCallback,SourceExtractor)
	 * @see #sendAndReceive(WebServiceMessageCallback,WebServiceMessageCallback)
	 */
	public void setDefaultUri(final String uri) {
		this.destinationProvider = new DestinationProvider() {

			public URI getDestination() {
				return URI.create(uri);
			}
		};
	}

	/**
	 * Returns the destination provider used on operations that do not have a URI
	 * parameter.
	 */
	public @Nullable DestinationProvider getDestinationProvider() {
		return this.destinationProvider;
	}

	/**
	 * Set the destination provider URI to be used on operations that do not have a URI
	 * parameter.
	 * <p>
	 * Typically, either this property is set, or {@link #setDefaultUri(String)}, but not
	 * both.
	 * @see #marshalSendAndReceive(Object)
	 * @see #marshalSendAndReceive(Object,WebServiceMessageCallback)
	 * @see #sendSourceAndReceiveToResult(Source,Result)
	 * @see #sendSourceAndReceiveToResult(Source,WebServiceMessageCallback,Result)
	 * @see #sendSourceAndReceive(Source,SourceExtractor)
	 * @see #sendSourceAndReceive(Source,WebServiceMessageCallback,SourceExtractor)
	 * @see #sendAndReceive(WebServiceMessageCallback,WebServiceMessageCallback)
	 */
	public void setDestinationProvider(DestinationProvider destinationProvider) {
		this.destinationProvider = destinationProvider;
	}

	/** Returns the marshaller for this template. */
	public @Nullable Marshaller getMarshaller() {
		return this.marshaller;
	}

	/** Sets the marshaller for this template. */
	public void setMarshaller(@Nullable Marshaller marshaller) {
		this.marshaller = marshaller;
	}

	/** Returns the unmarshaller for this template. */
	public @Nullable Unmarshaller getUnmarshaller() {
		return this.unmarshaller;
	}

	/** Sets the unmarshaller for this template. */
	public void setUnmarshaller(@Nullable Unmarshaller unmarshaller) {
		this.unmarshaller = unmarshaller;
	}

	/** Returns the fault message resolver for this template. */
	public @Nullable FaultMessageResolver getFaultMessageResolver() {
		return this.faultMessageResolver;
	}

	/**
	 * Sets the fault resolver for this template. Default is the
	 * {@link org.springframework.ws.soap.client.core.SoapFaultMessageResolver
	 * SoapFaultMessageResolver}, but may be set to {@code null} to disable fault
	 * handling.
	 */
	public void setFaultMessageResolver(FaultMessageResolver faultMessageResolver) {
		this.faultMessageResolver = faultMessageResolver;
	}

	/**
	 * Indicates whether the {@linkplain WebServiceConnection#hasError() connection}
	 * should be checked for error indicators ({@code true}), or whether these should be
	 * ignored ({@code false}). The default is {@code true}.
	 * <p>
	 * When using an HTTP transport, this property defines whether to check the HTTP
	 * response status code is in the 2xx Successful range. Both the SOAP specification
	 * and the WS-I Basic Profile define that a Web service must return a "200 OK" or "202
	 * Accepted" HTTP status code for a normal response. Setting this property to
	 * {@code false} allows this template to deal with non-conforming services.
	 * @see #hasError(WebServiceConnection, WebServiceMessage)
	 * @see <a href="http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383529">SOAP 1.1
	 * specification</a>
	 * @see <a href=
	 * "http://www.ws-i.org/Profiles/BasicProfile-1.1.html#HTTP_Success_Status_Codes">WS-I
	 * Basic Profile</a>
	 */
	public void setCheckConnectionForError(boolean checkConnectionForError) {
		this.checkConnectionForError = checkConnectionForError;
	}

	/**
	 * Indicates whether the {@linkplain FaultAwareWebServiceConnection#hasFault()
	 * connection} should be checked for fault indicators ({@code true}), or whether we
	 * should rely on the {@link FaultAwareWebServiceMessage#hasFault() message} only
	 * ({@code false}). The default is {@code true}.
	 * <p>
	 * When using an HTTP transport, this property defines whether to check the HTTP
	 * response status code for fault indicators. Both the SOAP specification and the WS-I
	 * Basic Profile define that a Web service must return a "500 Internal Server Error"
	 * HTTP status code if the response envelope is a Fault. Setting this property to
	 * {@code false} allows this template to deal with non-conforming services.
	 * @see #hasFault(WebServiceConnection,WebServiceMessage)
	 * @see <a href="http://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383529">SOAP 1.1
	 * specification</a>
	 * @see <a href=
	 * "http://www.ws-i.org/Profiles/BasicProfile-1.1.html#HTTP_Server_Error_Status_Codes">WS-I
	 * Basic Profile</a>
	 */
	public void setCheckConnectionForFault(boolean checkConnectionForFault) {
		this.checkConnectionForFault = checkConnectionForFault;
	}

	/**
	 * Returns the client interceptors to apply to all web service invocations made by
	 * this template.
	 * @return array of endpoint interceptors, or {@code null} if none
	 */
	public ClientInterceptor @Nullable [] getInterceptors() {
		return this.interceptors;
	}

	/**
	 * Sets the client interceptors to apply to all web service invocations made by this
	 * template.
	 * @param interceptors array of endpoint interceptors, or {@code null} if none
	 */
	public final void setInterceptors(ClientInterceptor[] interceptors) {
		this.interceptors = interceptors;
	}

	/**
	 * Initialize the default implementations for the template's strategies:
	 * {@link SoapFaultMessageResolver},
	 * {@link org.springframework.ws.soap.saaj.SaajSoapMessageFactory}, and
	 * {@link HttpUrlConnectionMessageSender}.
	 * @throws BeanInitializationException in case of initalization errors
	 * @see #setFaultMessageResolver(FaultMessageResolver)
	 * @see #setMessageFactory(WebServiceMessageFactory)
	 * @see #setMessageSender(WebServiceMessageSender)
	 */
	protected void initDefaultStrategies() {
		DefaultStrategiesHelper strategiesHelper = new DefaultStrategiesHelper(WebServiceTemplate.class);
		if (getMessageFactory() == null) {
			initMessageFactory(strategiesHelper);
		}
		if (ObjectUtils.isEmpty(getMessageSenders())) {
			initMessageSenders(strategiesHelper);
		}
		if (getFaultMessageResolver() == null) {
			initFaultMessageResolver(strategiesHelper);
		}
	}

	private void initMessageFactory(DefaultStrategiesHelper helper) throws BeanInitializationException {
		WebServiceMessageFactory messageFactory = helper.getDefaultStrategy(WebServiceMessageFactory.class);
		setMessageFactory(messageFactory);
	}

	private void initMessageSenders(DefaultStrategiesHelper helper) {
		List<WebServiceMessageSender> messageSenders = helper.getDefaultStrategies(WebServiceMessageSender.class);
		setMessageSenders(messageSenders.toArray(new WebServiceMessageSender[0]));
	}

	private void initFaultMessageResolver(DefaultStrategiesHelper helper) throws BeanInitializationException {
		FaultMessageResolver faultMessageResolver = helper.getDefaultStrategy(FaultMessageResolver.class);
		setFaultMessageResolver(faultMessageResolver);
	}

	//
	// Marshalling methods
	//

	@Override
	public @Nullable Object marshalSendAndReceive(final Object requestPayload) {
		return marshalSendAndReceive(requestPayload, null);
	}

	@Override
	public @Nullable Object marshalSendAndReceive(String uri, final Object requestPayload) {
		return marshalSendAndReceive(uri, requestPayload, null);
	}

	@Override
	public @Nullable Object marshalSendAndReceive(final @Nullable Object requestPayload,
			final @Nullable WebServiceMessageCallback requestCallback) {
		String defaultUri = getDefaultUri();
		Assert.notNull(defaultUri, "'defaultUri' must not be null");
		return marshalSendAndReceive(defaultUri, requestPayload, requestCallback);
	}

	@Override
	public @Nullable Object marshalSendAndReceive(String uri, final @Nullable Object requestPayload,
			final @Nullable WebServiceMessageCallback requestCallback) {
		return sendAndReceive(uri, new WebServiceMessageCallback() {

			public void doWithMessage(WebServiceMessage request) throws IOException, TransformerException {
				if (requestPayload != null) {
					Marshaller marshaller = getMarshaller();
					if (marshaller == null) {
						throw new IllegalStateException(
								"No marshaller registered. Check configuration of WebServiceTemplate.");
					}
					MarshallingUtils.marshal(marshaller, requestPayload, request);
				}
				if (requestCallback != null) {
					requestCallback.doWithMessage(request);
				}
			}
		}, new WebServiceMessageExtractor<>() {

			public @Nullable Object extractData(WebServiceMessage response) throws IOException {
				Unmarshaller unmarshaller = getUnmarshaller();
				if (unmarshaller == null) {
					throw new IllegalStateException(
							"No unmarshaller registered. Check configuration of WebServiceTemplate.");
				}
				return MarshallingUtils.unmarshal(unmarshaller, response);
			}
		});
	}

	//
	// Result-handling methods
	//

	@Override
	public boolean sendSourceAndReceiveToResult(Source requestPayload, Result responseResult) {
		return sendSourceAndReceiveToResult(requestPayload, null, responseResult);
	}

	@Override
	public boolean sendSourceAndReceiveToResult(String uri, Source requestPayload, Result responseResult) {
		return sendSourceAndReceiveToResult(uri, requestPayload, null, responseResult);
	}

	@Override
	public boolean sendSourceAndReceiveToResult(Source requestPayload,
			@Nullable WebServiceMessageCallback requestCallback, final Result responseResult) {
		String defaultUri = getDefaultUri();
		Assert.notNull(defaultUri, "'defaultUri' must not be null");
		return sendSourceAndReceiveToResult(defaultUri, requestPayload, requestCallback, responseResult);
	}

	@Override
	public boolean sendSourceAndReceiveToResult(String uri, Source requestPayload,
			@Nullable WebServiceMessageCallback requestCallback, final Result responseResult) {
		try {
			final Transformer transformer = createTransformer();
			Boolean retVal = doSendAndReceive(uri, transformer, requestPayload, requestCallback,
					new SourceExtractor<>() {

						public Boolean extractData(@Nullable Source source) throws IOException, TransformerException {
							if (source != null) {
								transformer.transform(source, responseResult);
							}
							return Boolean.TRUE;
						}
					});
			return retVal != null && retVal;
		}
		catch (TransformerConfigurationException ex) {
			throw new WebServiceTransformerException("Could not create transformer", ex);
		}
	}

	//
	// Source-handling methods
	//

	@Override
	public <T> @Nullable T sendSourceAndReceive(final Source requestPayload,
			final SourceExtractor<T> responseExtractor) {
		return sendSourceAndReceive(requestPayload, null, responseExtractor);
	}

	@Override
	public <T> @Nullable T sendSourceAndReceive(String uri, final Source requestPayload,
			final SourceExtractor<T> responseExtractor) {
		return sendSourceAndReceive(uri, requestPayload, null, responseExtractor);
	}

	@Override
	public <T> @Nullable T sendSourceAndReceive(final Source requestPayload,
			final @Nullable WebServiceMessageCallback requestCallback, final SourceExtractor<T> responseExtractor) {
		String defaultUri = getDefaultUri();
		Assert.notNull(defaultUri, "'defaultUri' must not be null");
		return sendSourceAndReceive(defaultUri, requestPayload, requestCallback, responseExtractor);
	}

	@Override
	public <T> @Nullable T sendSourceAndReceive(String uri, final Source requestPayload,
			final @Nullable WebServiceMessageCallback requestCallback, final SourceExtractor<T> responseExtractor) {

		try {
			return doSendAndReceive(uri, createTransformer(), requestPayload, requestCallback, responseExtractor);
		}
		catch (TransformerConfigurationException ex) {
			throw new WebServiceTransformerException("Could not create transformer", ex);
		}
	}

	private <T> @Nullable T doSendAndReceive(String uri, final Transformer transformer, final Source requestPayload,
			final @Nullable WebServiceMessageCallback requestCallback, final SourceExtractor<T> responseExtractor) {
		Assert.notNull(responseExtractor, "responseExtractor must not be null");
		return sendAndReceive(uri, new WebServiceMessageCallback() {
			public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException {
				transformer.transform(requestPayload, message.getPayloadResult());
				if (requestCallback != null) {
					requestCallback.doWithMessage(message);
				}
			}
		}, new SourceExtractorMessageExtractor<>(responseExtractor));
	}

	//
	// WebServiceMessage-handling methods
	//

	@Override
	public boolean sendAndReceive(WebServiceMessageCallback requestCallback,
			WebServiceMessageCallback responseCallback) {
		String defaultUri = getDefaultUri();
		Assert.notNull(defaultUri, "'defaultUri' must not be null");
		return sendAndReceive(defaultUri, requestCallback, responseCallback);
	}

	@Override
	public boolean sendAndReceive(String uri, WebServiceMessageCallback requestCallback,
			WebServiceMessageCallback responseCallback) {
		Assert.notNull(responseCallback, "responseCallback must not be null");
		Boolean result = sendAndReceive(uri, requestCallback,
				new WebServiceMessageCallbackMessageExtractor(responseCallback));
		return result != null && result;
	}

	@Override
	public <T> @Nullable T sendAndReceive(@Nullable WebServiceMessageCallback requestCallback,
			WebServiceMessageExtractor<T> responseExtractor) {
		String defaultUri = getDefaultUri();
		Assert.notNull(defaultUri, "'defaultUri' must not be null");
		return sendAndReceive(defaultUri, requestCallback, responseExtractor);
	}

	@Override
	public <T> @Nullable T sendAndReceive(String uriString, @Nullable WebServiceMessageCallback requestCallback,
			WebServiceMessageExtractor<T> responseExtractor) {
		Assert.notNull(responseExtractor, "'responseExtractor' must not be null");
		Assert.hasLength(uriString, "'uri' must not be empty");
		TransportContext previousTransportContext = TransportContextHolder.getTransportContext();
		WebServiceConnection connection = null;
		try {
			connection = createConnection(URI.create(uriString));
			TransportContextHolder.setTransportContext(new DefaultTransportContext(connection));
			MessageContext messageContext = new DefaultMessageContext(getMessageFactory());

			return doSendAndReceive(messageContext, connection, requestCallback, responseExtractor);
		}
		catch (TransportException ex) {
			throw new WebServiceTransportException("Could not use transport: " + ex.getMessage(), ex);
		}
		catch (IOException ex) {
			throw new WebServiceIOException("I/O error: " + ex.getMessage(), ex);
		}
		finally {
			if (connection != null) {
				TransportUtils.closeConnection(connection);
			}
			TransportContextHolder.setTransportContext(previousTransportContext);
		}
	}

	/**
	 * Sends and receives a {@link MessageContext}. Sends the
	 * {@link MessageContext#getRequest() request message}, and received to the
	 * {@link MessageContext#getResponse() repsonse message}. Invocates the defined
	 * {@link #setInterceptors(ClientInterceptor[]) interceptors} as part of the process.
	 * @param messageContext the message context
	 * @param connection the connection to use
	 * @param requestCallback the requestCallback to be used for manipulating the request
	 * message
	 * @param responseExtractor object that will extract results
	 * @return an arbitrary result object, as returned by the
	 * {@code WebServiceMessageExtractor}
	 * @throws WebServiceClientException if there is a problem sending or receiving the
	 * message
	 * @throws IOException in case of I/O errors
	 */
	@SuppressWarnings("unchecked")
	protected <T> @Nullable T doSendAndReceive(MessageContext messageContext, WebServiceConnection connection,
			@Nullable WebServiceMessageCallback requestCallback, WebServiceMessageExtractor<T> responseExtractor)
			throws IOException {
		int interceptorIndex = -1;
		try {
			if (requestCallback != null) {
				requestCallback.doWithMessage(messageContext.getRequest());
			}
			// Apply handleRequest of registered interceptors
			boolean intercepted = false;
			if (this.interceptors != null) {
				for (int i = 0; i < this.interceptors.length; i++) {
					interceptorIndex = i;
					if (!this.interceptors[i].handleRequest(messageContext)) {
						intercepted = true;
						break;
					}
				}
			}
			// no send/receive if an interceptor has set a response or if the chain
			// has been interrupted
			if (!messageContext.hasResponse() && !intercepted) {
				sendRequest(connection, messageContext.getRequest());
				if (hasError(connection, messageContext.getRequest())) {
					Object fallback = handleError(connection, messageContext.getRequest());
					triggerAfterCompletion(interceptorIndex, messageContext, null);
					return (T) fallback;
				}
				WebServiceMessage response = connection.receive(getMessageFactory());
				messageContext.setResponse(response);
			}
			logResponse(messageContext);
			if (messageContext.hasResponse()) {
				if (!hasFault(connection, messageContext.getResponse())) {
					triggerHandleResponse(interceptorIndex, messageContext);
					T result = responseExtractor.extractData(messageContext.getResponse());
					triggerAfterCompletion(interceptorIndex, messageContext, null);
					return result;
				}
				else {
					triggerHandleFault(interceptorIndex, messageContext);
					Object fallback = handleFault(connection, messageContext);
					triggerAfterCompletion(interceptorIndex, messageContext, null);
					return (T) fallback;
				}
			}
			else {
				triggerAfterCompletion(interceptorIndex, messageContext, null);
				return null;
			}
		}
		catch (TransformerException ex) {
			triggerAfterCompletion(interceptorIndex, messageContext, ex);
			throw new WebServiceTransformerException("Transformation error: " + ex.getMessage(), ex);
		}
		catch (RuntimeException | IOException ex) {
			// Trigger after-completion for thrown exception.
			triggerAfterCompletion(interceptorIndex, messageContext, ex);
			throw ex;
		}
	}

	/** Sends the request in the given message context over the connection. */
	private void sendRequest(WebServiceConnection connection, WebServiceMessage request) throws IOException {
		if (sentMessageTracingLogger.isTraceEnabled()) {
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			request.writeTo(os);
			sentMessageTracingLogger.trace("Sent request [" + os.toString(StandardCharsets.UTF_8) + "]");
		}
		else if (sentMessageTracingLogger.isDebugEnabled()) {
			sentMessageTracingLogger.debug("Sent request [" + request + "]");
		}
		connection.send(request);
	}

	/**
	 * Determines whether the given connection or message context has an error.
	 * <p>
	 * This implementation checks the {@link WebServiceConnection#hasError() connection}
	 * first. If it indicates an error, it makes sure that it is not a
	 * {@link FaultAwareWebServiceConnection#hasFault() fault}.
	 * @param connection the connection (possibly a {@link FaultAwareWebServiceConnection}
	 * @param request the response message (possibly a {@link FaultAwareWebServiceMessage}
	 * @return {@code true} if the connection has an error; {@code false} otherwise
	 * @throws IOException in case of I/O errors
	 */
	protected boolean hasError(WebServiceConnection connection, WebServiceMessage request) throws IOException {
		if (this.checkConnectionForError && connection.hasError()) {
			// could be a fault
			if (this.checkConnectionForFault && connection instanceof FaultAwareWebServiceConnection faultConnection) {
				return !(faultConnection.hasFault() && request instanceof FaultAwareWebServiceMessage);
			}
			else {
				return true;
			}
		}
		return false;
	}

	/**
	 * Handles an error on the given connection. The default implementation throws a
	 * {@link WebServiceTransportException}.
	 * @param connection the erroneous connection
	 * @param request the corresponding request message
	 * @return the object to be returned from
	 * {@link #sendAndReceive(String,WebServiceMessageCallback, WebServiceMessageExtractor)},
	 * if any
	 */
	protected Object handleError(WebServiceConnection connection, WebServiceMessage request) throws IOException {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Received error for request [" + request + "]");
		}
		throw new WebServiceTransportException(connection.getErrorMessage());
	}

	private void logResponse(MessageContext messageContext) throws IOException {
		if (messageContext.hasResponse()) {
			if (receivedMessageTracingLogger.isTraceEnabled()) {
				ByteArrayOutputStream requestStream = new ByteArrayOutputStream();
				messageContext.getRequest().writeTo(requestStream);
				ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
				messageContext.getResponse().writeTo(responseStream);
				receivedMessageTracingLogger
					.trace("Received response [" + responseStream.toString(StandardCharsets.UTF_8) + "] for request ["
							+ requestStream.toString(StandardCharsets.UTF_8) + "]");
			}
			else if (receivedMessageTracingLogger.isDebugEnabled()) {
				receivedMessageTracingLogger.debug("Received response [" + messageContext.getResponse()
						+ "] for request [" + messageContext.getRequest() + "]");
			}
		}
		else {
			if (receivedMessageTracingLogger.isDebugEnabled()) {
				receivedMessageTracingLogger
					.debug("Received no response for request [" + messageContext.getRequest() + "]");
			}
		}
	}

	/**
	 * Determines whether the given connection or message has a fault.
	 * <p>
	 * This implementation checks the {@link FaultAwareWebServiceConnection#hasFault()
	 * connection} if the {@link #setCheckConnectionForFault(boolean)
	 * checkConnectionForFault} property is true, and defaults to the
	 * {@link FaultAwareWebServiceMessage#hasFault() message} otherwise.
	 * @param connection the connection (possibly a {@link FaultAwareWebServiceConnection}
	 * @param response the response message (possibly a
	 * {@link FaultAwareWebServiceMessage}
	 * @return {@code true} if either the connection or the message has a fault;
	 * {@code false} otherwise
	 * @throws IOException in case of I/O errors
	 */
	protected boolean hasFault(WebServiceConnection connection, WebServiceMessage response) throws IOException {
		if (this.checkConnectionForFault && connection instanceof FaultAwareWebServiceConnection faultConnection) {
			// check whether the connection has a fault (i.e. status code 500 in HTTP)
			if (!faultConnection.hasFault()) {
				return false;
			}
		}
		if (response instanceof FaultAwareWebServiceMessage faultMessage) {
			// either the connection has a fault, or checkConnectionForFault is false:
			// let's verify the fault
			return faultMessage.hasFault();
		}
		return false;
	}

	/**
	 * Trigger handleResponse on the defined ClientInterceptors. Will just invoke said
	 * method on all interceptors whose handleRequest invocation returned {@code true}, in
	 * addition to the last interceptor who returned {@code false}.
	 * @param interceptorIndex index of last interceptor that was called
	 * @param messageContext the message context, whose request and response are filled
	 * @see ClientInterceptor#handleResponse(MessageContext)
	 * @see ClientInterceptor#handleFault(MessageContext)
	 */
	private void triggerHandleResponse(int interceptorIndex, MessageContext messageContext) {
		if (messageContext.hasResponse() && this.interceptors != null) {
			for (int i = interceptorIndex; i >= 0; i--) {
				if (!this.interceptors[i].handleResponse(messageContext)) {
					break;
				}
			}
		}
	}

	/**
	 * Trigger handleFault on the defined ClientInterceptors. Will just invoke said method
	 * on all interceptors whose handleRequest invocation returned {@code true}, in
	 * addition to the last interceptor who returned {@code false}.
	 * @param interceptorIndex index of last interceptor that was called
	 * @param messageContext the message context, whose request and response are filled
	 * @see ClientInterceptor#handleResponse(MessageContext)
	 * @see ClientInterceptor#handleFault(MessageContext)
	 */
	private void triggerHandleFault(int interceptorIndex, MessageContext messageContext) {
		if (messageContext.hasResponse() && this.interceptors != null) {
			for (int i = interceptorIndex; i >= 0; i--) {
				if (!this.interceptors[i].handleFault(messageContext)) {
					break;
				}
			}
		}
	}

	/**
	 * Trigger afterCompletion callbacks on the mapped ClientInterceptors. Will just
	 * invoke afterCompletion for all interceptors whose handleRequest invocation has
	 * successfully completed and returned true, in addition to the last interceptor who
	 * returned {@code false}.
	 * @param interceptorIndex index of last interceptor that successfully completed
	 * @param messageContext the message context
	 * @param ex exception thrown on handler execution, or {@code null} if none
	 * @see ClientInterceptor#afterCompletion
	 */
	private void triggerAfterCompletion(int interceptorIndex, MessageContext messageContext, @Nullable Exception ex)
			throws WebServiceClientException {
		if (this.interceptors != null) {
			for (int i = interceptorIndex; i >= 0; i--) {
				try {
					this.interceptors[i].afterCompletion(messageContext, ex);
				}
				catch (Exception interceptorEx) {
					logger.error("ClientInterceptor.afterCompletion threw exception", ex);
				}
			}
		}
	}

	/**
	 * Handles an fault in the given response message. The default implementation invokes
	 * the {@link FaultMessageResolver fault resolver} if registered, or invokes
	 * {@link #handleError(WebServiceConnection, WebServiceMessage)} otherwise.
	 * @param connection the faulty connection
	 * @param messageContext the message context
	 * @return the object to be returned from
	 * {@link #sendAndReceive(String,WebServiceMessageCallback, WebServiceMessageExtractor)},
	 * if any
	 */
	protected @Nullable Object handleFault(WebServiceConnection connection, MessageContext messageContext)
			throws IOException {
		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Received Fault message for request [" + messageContext.getRequest() + "]");
		}
		if (getFaultMessageResolver() != null) {
			getFaultMessageResolver().resolveFault(messageContext.getResponse());
			return null;
		}
		else {
			return handleError(connection, messageContext.getRequest());
		}
	}

	/**
	 * Adapter to enable use of a WebServiceMessageCallback inside a
	 * WebServiceMessageExtractor.
	 */
	private static final class WebServiceMessageCallbackMessageExtractor
			implements WebServiceMessageExtractor<Boolean> {

		private final WebServiceMessageCallback callback;

		private WebServiceMessageCallbackMessageExtractor(WebServiceMessageCallback callback) {
			this.callback = callback;
		}

		@Override
		public Boolean extractData(WebServiceMessage message) throws IOException, TransformerException {
			this.callback.doWithMessage(message);
			return Boolean.TRUE;
		}

	}

	/** Adapter to enable use of a SourceExtractor inside a WebServiceMessageExtractor. */
	private static final class SourceExtractorMessageExtractor<T> implements WebServiceMessageExtractor<T> {

		private final SourceExtractor<T> sourceExtractor;

		private SourceExtractorMessageExtractor(SourceExtractor<T> sourceExtractor) {
			this.sourceExtractor = sourceExtractor;
		}

		@Override
		public @Nullable T extractData(WebServiceMessage message) throws IOException, TransformerException {
			return this.sourceExtractor.extractData(message.getPayloadSource());
		}

	}

}
